package com.deange.ropeprogressview;

import ohos.agp.animation.Animator;
import ohos.agp.animation.AnimatorValue;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.render.Path;
import ohos.agp.render.PathEffect;
import ohos.agp.text.Font;
import ohos.agp.utils.Color;
import ohos.agp.utils.Rect;
import ohos.agp.utils.TextAlignment;
import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;

/**
 * RopeProgressBar
 *
 * @author hw
 * @since 2021/5/29
 */
public class RopeProgressBar extends Component implements Component.EstimateSizeListener, Component.DrawTask {
    private final Paint mBubblePaint = new Paint();
    private final Paint mLinesPaint = new Paint();
    private final Paint mTextPaint = new Paint();

    private int mProgress;
    private int mMax;
    private int mPrimaryColor;
    private int mSecondaryColor;
    private float mSlack;
    private boolean mDynamicLayout;
    private ProgressFormatter mFormatter;

    private Rect mBounds = new Rect();
    private final Path mBubble = new Path();
    private final Path mTriangle = new Path();

    private static final Animator.TimelineCurve INTERPOLATOR = new DampingInterpolator(5);
    private AnimatorValue mAnimator;
    private float mBounceX;
    private int mStartProgress;
    private boolean mDeferred;
    private boolean mSlackSetByUser;

    private final Runnable mRequestLayoutRunnable = this::postLayout;

    public RopeProgressBar(Context context, AttrSet attrs) {
        this(context, attrs, null);
    }

    public RopeProgressBar(
            Context context,
            AttrSet attrs,
            String defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mBubblePaint.setAntiAlias(true);
        mLinesPaint.setAntiAlias(true);
        mTextPaint.setAntiAlias(true);

        PixelUtil.initContext(context);

        int max = 0;
        int progress = 0;

        float width = PixelUtil.vp2px(8);
        float slack = PixelUtil.vp2px(32);
        boolean dynamicLayout = false;

        int primaryColor = 0xFF009688;
        int secondaryColor = 0xFFDADADA;

        max = AttrUtils.getInteger(attrs, "ropeMax", max);
        progress = AttrUtils.getInteger(attrs, "ropeProgress", progress);
        primaryColor = AttrUtils.getColor(attrs, "ropePrimaryColor", primaryColor);
        secondaryColor = AttrUtils.getColor(attrs, "ropeSecondaryColor", secondaryColor);
        slack = AttrUtils.getFloat(attrs, "ropeSlack", slack);
        width = AttrUtils.getFloat(attrs, "ropeStrokeWidth", width);
        dynamicLayout = AttrUtils.getBoolean(attrs, "ropeDynamicLayout", false);
        mSlackSetByUser = attrs.getAttr("ropeSlack").isPresent();

        mPrimaryColor = primaryColor;
        mSecondaryColor = secondaryColor;
        mSlack = slack;
        mDynamicLayout = dynamicLayout;

        mLinesPaint.setStrokeWidth(width);
        mLinesPaint.setStyle(Paint.Style.STROKE_STYLE);
        mLinesPaint.setStrokeCap(Paint.StrokeCap.ROUND_CAP);

        mBubblePaint.setColor(Color.WHITE);
        mBubblePaint.setStyle(Paint.Style.FILL_STYLE);
        mBubblePaint.setPathEffect(new PathEffect(PixelUtil.vp2px(2)));
        mTextPaint.setColor(Color.BLACK);
        mTextPaint.setTextSize((int) PixelUtil.fp2px(18));
        mTextPaint.setTextAlign(TextAlignment.CENTER);
        mTextPaint.setFont(Font.SANS_SERIF);

        setMax(max);
        setProgress(progress);

        setEstimateSizeListener(this);
        addDrawTask(this);
    }

    private final EventHandler myHandler = new EventHandler(EventRunner.create());

    private void dynamicRequestLayout() {
        if (mDynamicLayout) {
            // We need to calculate our new height, since the progress affect the slack
            if (EventRunner.getMainEventRunner() == EventRunner.current()) {
                mRequestLayoutRunnable.run();
            } else {
                myHandler.postTask(mRequestLayoutRunnable);
            }
        }
    }

    @Override
    public boolean onEstimateSize(int widthEstimateConfig, int heightEstimateConfig) {
        if (!mSlackSetByUser) {
            // Slack is unset, default it to 10% of the width of the view
            mSlack = MeasureSpec.getSize(widthEstimateConfig) * 0.1f;
        }

        // Recalculate how tall the text needs to be, width is ignored
        final String progress = getBubbleText();
        mBounds = mTextPaint.getTextBounds(progress);
        final int bubbleHeight = (int) Math.ceil(getBubbleVerticalDisplacement());
        final float slack = (mDynamicLayout) ? getCurrentSlackHeight() : getSlack();
        final float strokeWidth = getStrokeWidth();
        final int dh = (int) Math.ceil(getPaddingTop() + getPaddingBottom() + (double) strokeWidth + slack);
        int width = Component.EstimateSpec.getSize(widthEstimateConfig);
        int height = Component.EstimateSpec.getSize(heightEstimateConfig);
        setEstimatedSize(EstimateSpec.getChildSizeWithMode(width - (int) PixelUtil.vp2px(16),
                width, EstimateSpec.NOT_EXCEED), EstimateSpec.getChildSizeWithMode(dh + bubbleHeight +
                (int) PixelUtil.vp2px(2), height, EstimateSpec.NOT_EXCEED));
        LogUtil.debug("cpf", "" + getEstimatedWidth() + ":"
                + getEstimatedHeight() + ":" + width + ":" + height);
        // Make the triangle Path
        mTriangle.reset();
        mTriangle.moveTo(0, 0);
        mTriangle.lineTo(getTriangleWidth(), 0);
        mTriangle.lineTo(getTriangleWidth() / 2f, getTriangleHeight());
        mTriangle.lineTo(0, 0);
        return true;
    }

    @Override
    public void addDrawTask(DrawTask task) {
        super.addDrawTask(task);
        onDraw(this, mCanvasForTaskOverContent);
    }

    @SuppressWarnings("UnnecessaryLocalVariable")
    @Override
    public void onDraw(Component component, Canvas canvas) {
        final float radius = getStrokeWidth() / 2;
        final float bubbleDisplacement = getBubbleVerticalDisplacement();
        final float top = (float) (getPaddingTop() + (double) radius + bubbleDisplacement);
        final float left = (float) (getPaddingLeft() + (double) radius);
        final float end = (float) (getWidth() - getPaddingRight() - (double) radius);
        final float max = getMax();
        final float offset = (max == 0) ? 0 : (getProgress() / max);
        final float slackHeight = getCurrentSlackHeight();
        final float progressEnd =
                clamp((float) (lerp(left, end, offset) + ((double) mBounceX * perp(offset))), left, end);

        // Draw the secondary background line
        mLinesPaint.setColor(new Color(mSecondaryColor));
        canvas.drawLine(progressEnd, (float) (top + (double) slackHeight), end, top, mLinesPaint);

        // Draw the primary progress line
        mLinesPaint.setColor(new Color(mPrimaryColor));
        if (Math.abs(progressEnd) == Math.abs(left)) {
            // Draw the highlghted part as small as possible
            mLinesPaint.setStyle(Paint.Style.FILL_STYLE);
            canvas.drawCircle(left, top, radius, mLinesPaint);
            mLinesPaint.setStyle(Paint.Style.STROKE_STYLE);

        } else {
            canvas.drawLine(left, top, progressEnd, (float) ((double) top + slackHeight), mLinesPaint);
        }

        final String progress = getBubbleText();
        mBounds = mTextPaint.getTextBounds(progress);

        // Draw the bubble text background
        final float bubbleWidth = getBubbleWidth();
        final float bubbleHeight = getBubbleHeight();
        mBubble.reset();
        mBubble.addRect(0, 0, bubbleWidth, bubbleHeight, Path.Direction.CLOCK_WISE);

        final float bubbleTop = Math.max(slackHeight, 0);
        final float bubbleLeft = clamp(
                (float) ((double) progressEnd - ((double) bubbleWidth / 2)),
                0,
                (float) (getWidth() - (double) bubbleWidth));

        final int saveCount = canvas.save();
        canvas.translate(bubbleLeft, bubbleTop);

        canvas.drawPath(mBubble, mBubblePaint);

        // Draw the triangle part of the bubble
        final float triangleTop = bubbleHeight;
        final float triangleLeft = clamp(
                (float) (progressEnd - ((double) getTriangleWidth() / 2) - (double) bubbleLeft),
                0,
                (float) (getWidth() - (double) getTriangleWidth()));

        mTriangle.offset(triangleLeft, triangleTop);
        canvas.drawPath(mTriangle, mBubblePaint);
        mTriangle.offset(-triangleLeft, -triangleTop);

        // Draw the progress text part of the bubble
        final float textX = (float) ((double) bubbleWidth / 2);
        final float textY = (float) ((double) bubbleHeight - PixelUtil.vp2px(8));

        canvas.drawText(mTextPaint, progress, textX, textY);

        canvas.restoreToCount(saveCount);
    }

    private float getCurrentSlackHeight() {
        final float max = getMax();
        final float offset = (max == 0) ? 0 : (getProgress() / max);
        return getSlack() * perp(offset);
    }

    private float getBubbleVerticalDisplacement() {
        return (float) ((double) getBubbleMargin() + (double) getBubbleHeight() + (double) getTriangleHeight());
    }

    public float getBubbleMargin() {
        return PixelUtil.vp2px(4);
    }

    public float getBubbleWidth() {
        return (float) (mBounds.getWidth() + /* padding */ (double) PixelUtil.vp2px(16));
    }

    public float getBubbleHeight() {
        return (float) (mBounds.getHeight() + /* padding */ (double) PixelUtil.vp2px(16));
    }

    public float getTriangleWidth() {
        return PixelUtil.vp2px(12);
    }

    public float getTriangleHeight() {
        return PixelUtil.vp2px(6);
    }

    public String getBubbleText() {
        if (mFormatter != null) {
            return mFormatter.getFormattedText(getProgress(), getMax());

        } else {
            final int progress = (int) (100 * getProgress() / (float) getMax());
            return progress + "%";
        }
    }

    public void defer() {
        if (!mDeferred) {
            mDeferred = true;
            mStartProgress = getProgress();
        }
    }

    public void endDefer() {
        if (mDeferred) {
            mDeferred = false;
            bounceAnimation(mStartProgress);
        }
    }

    public void setProgress(int progress) {
        LogUtil.debug("cpf>>>", "setProgress: " + progress);
        progress = (int) clamp(progress, 0, getMax());
        if (progress == mProgress) {
            return;
        }
        if (!mDeferred) {
            bounceAnimation(progress);
        }
        dynamicRequestLayout();
        mProgress = progress;
        LogUtil.debug("cpf>>>", "setProgress:invalidate " + progress);
        invalidate();
    }

    public void animateProgress(final int progress) {
        // Speed of animation is interpolated from 0 --> MAX in 2s
        // Minimum time duration is 500ms because anything faster than that is waaaay too quick
        final int startProgress = getProgress();
        final int endProgress = (int) clamp(progress, 0, getMax());
        final int diff = Math.abs(getProgress() - endProgress);
        final long duration = Math.max(500L, (long) (2000L * (diff / (float) getMax())));
        final AnimatorValue animator = new AnimatorValue();
        animator.setDuration(duration);
        animator.setCurveType(Animator.CurveType.ACCELERATE);
        animator.setStateChangedListener(new Animator.StateChangedListener() {
            @Override
            public void onStart(Animator animator) {
                defer();
            }

            @Override
            public void onStop(Animator animator) {
            }

            @Override
            public void onCancel(Animator animator) {
            }

            @Override
            public void onEnd(Animator animator) {
                bounceAnimation(startProgress);
                endDefer();
            }

            @Override
            public void onPause(Animator animator) {
            }

            @Override
            public void onResume(Animator animator) {
            }
        });
        animator.setValueUpdateListener((animatorValue, v) -> {
            int valueProgress = (int) (startProgress + v * (endProgress - startProgress));
            setProgress(valueProgress);
        });
        animator.start();
    }

    private void bounceAnimation(final int startProgress) {
        // Moving the progress by at least 1/4 of the total distance will invoke
        // the "max" possible slack bouncing at the end progress value
        final int diff = Math.abs(startProgress - mProgress);
        final float diffPercent = Math.min(1f, 4 * diff / (float) getMax());
        if (mAnimator != null) {
            mAnimator.cancel();
        }
        mAnimator = new AnimatorValue();
        mAnimator.setCurve(INTERPOLATOR);
        mAnimator.setDuration(1000L);
        mAnimator.setValueUpdateListener((animatorValue, v) -> {
            mBounceX = v * diffPercent * getTriangleWidth();
            invalidate();
        });
        mAnimator.start();
    }

    public int getProgress() {
        return mProgress;
    }

    public void setMax(int max) {
        max = Math.max(0, max);
        if (max != mMax) {
            dynamicRequestLayout();
            mMax = max;
            if (mProgress > max) {
                mProgress = max;
            }
            invalidate();
        }
    }

    public int getMax() {
        return mMax;
    }

    public void setDynamicLayout(final boolean isDynamic) {
        if (mDynamicLayout != isDynamic) {
            mDynamicLayout = isDynamic;
            postLayout();
            invalidate();
        }
    }

    public void setPrimaryColor(final int color) {
        mPrimaryColor = color;
        invalidate();
    }

    public int getPrimaryColor() {
        return mPrimaryColor;
    }

    public void setSecondaryColor(final int color) {
        mSecondaryColor = color;
        invalidate();
    }

    public int getSecondaryColor() {
        return mSecondaryColor;
    }

    public void setSlack(final float slack) {
        mSlack = slack;
        postLayout();
        invalidate();
    }

    public float getSlack() {
        return mSlack;
    }

    public void setStrokeWidth(final float width) {
        mLinesPaint.setStrokeWidth(width);
        postLayout();
        invalidate();
    }

    public float getStrokeWidth() {
        return mLinesPaint.getStrokeWidth();
    }

    public void setTypeface(final Font typeface) {
        mTextPaint.setFont(typeface);
        postLayout();
        invalidate();
    }

    public void setTextPaint(final Paint paint) {
        mTextPaint.set(paint);
        postLayout();
        invalidate();
    }

    /**
     * TextPaint
     *
     * @return Return a copy so that fields can only be modified through
     */
    public Paint getTextPaint() {
        return new Paint(mTextPaint);
    }

    public void setProgressFormatter(final ProgressFormatter formatter) {
        mFormatter = formatter;
        postLayout();
        invalidate();
    }

    private float clamp(final float value, final float min, final float max) {
        return (float) Math.max(min, Math.min((double) max, value));
    }

    private float perp(float t) {
        // eh, could be more mathematically accurate to use a catenary function,
        // but the max difference between the two is only 0.005
        return (float) (-Math.pow(2 * (double) t - 1, 2) + 1);
    }

    private float lerp(float v0, float v1, float t) {
        return (t == 1) ? v1 : (float) (v0 + (double) t * ((double) v1 - v0));
    }

}
